// file: arch/hardware/serial.c
// time: 2021.12.22
// autor: jiangxinpeng
// time: 2021.12.22
// copyright: (C) 2020-2050 by jiangxinpeng,All right are reserved.

#include <arch/config.h>
#include <arch/interrupt.h>
#include <arch/io.h>
#include <arch/serial.h>
#include <os/debug.h>
#include <lib/stdio.h>

// global serial hardware
serial_hardware_t serial[COM_NUM_MAX];

void SerialHardwareInit()
{
    serial_hardware_t *obj = serial;
    int i;
    uint16_t iobase;
    uint8_t irq;

    for (i = 0; i < COM_NUM_MAX; i++)
    {
        obj = &serial[i];

        // select serial iobase
        switch (i)
        {
        case COM1:
            iobase = COM1_BASE;
            break;
        case COM2:
            iobase = COM2_BASE;
            break;
        case COM3:
            iobase = COM3_BASE;
            break;
        case COM4:
            iobase = COM4_BASE;
            break;
        }

        // select serial irq
        switch (i)
        {
        // COM1 and COM3 use irq4
        case COM1:
        case COM3:
            irq = IRQ4_SERIAL1;
            break;
        // COM2 and COM4 use irq3
        case COM2:
        case COM4:
            irq = IRQ3_SERIAL2;
            break;
        }

        obj->iobase = iobase;
        obj->data_reg = iobase + 0;
        obj->divisor_low_reg = iobase + 0;
        obj->int_enable_reg = iobase + 1;
        obj->divisor_high_reg = iobase + 1;
        obj->int_identify_reg = iobase + 2;
        obj->fifo_reg = iobase + 2;
        obj->line_ctrl_reg = iobase + 3;
        obj->modem_ctrl_reg = iobase + 4;
        obj->line_status_reg = iobase + 5;
        obj->modem_status_reg = iobase + 6;
        obj->scratch_reg = iobase + 7;

        obj->irq = irq;

        // set baud value
        Out8(obj->line_ctrl_reg, (uint8_t)LINE_DLAB);
        Out8(obj->divisor_low_reg, (uint8_t)DEFAULT_DIVISION_VALUE);
        Out8(obj->divisor_high_reg, (uint8_t)(DEFAULT_DIVISION_VALUE >> 8));
        Out8(obj->line_ctrl_reg, 0);

        // set word,stop bit and no parity
        Out8(obj->line_ctrl_reg, (uint8_t)(LINE_WORD_LEN_8 | LINE_STOP_BIT_1 | LINE_PARITY_NO));

        // test
        Out8(obj->modem_ctrl_reg, (uint8_t)MOMED_LOOKBACK_MODE);
        while (!In8(obj->line_status_reg) & LINE_STATUS_EMPTY_DATA_HOLDING)
            ;
        ;
        Out8(obj->data_reg, 0xAE);
        while (!In8(obj->line_status_reg) & LINE_STATUS_DATA_READY)
            ;
        if (In8(obj->data_reg) != (uint8_t)0xAE)
            continue;

        // disable interrupt
        Out8(obj->int_enable_reg, 0);

        // enable fifo,clear buffer
        Out8(obj->fifo_reg, (uint8_t)(FIFO_ENABLE | FIFO_CLEAR_TRANSMIT | FIFO_CLEAR_RECEIVE | FIFO_ENABLE_64 | FIFO_TRIGGER_14));

        // no modem ctrl
        Out8(obj->modem_ctrl_reg, (uint8_t)0x00);

        // no scratch
        Out8(obj->scratch_reg, (uint8_t)0x00);
    }
}

static int SerialSend(serial_hardware_t *obj, char data)
{
    int timeout = 0x1000;
    while (!(In8(obj->line_status_reg) && (uint8_t)LINE_STATUS_EMPTY_TRANSMITTER_HOLDING) && timeout--)
        ;

    if (!timeout)
        return -1;
    Out8(obj->data_reg, data);
    return 0;
}

static int SerialReceived(serial_hardware_t *obj, char *data)
{
    int timeout = 0x1000;
    while (!(In8(obj->line_status_reg) & (uint8_t)LINE_STATUS_DATA_READY) && timeout--)
        ;
    if (!timeout)
        return -1;
    *data = In8(obj->data_reg);
    return 0;
}

void SerialPutChar(int port, char ch)
{
    if (ch == '\n')
        SerialSend(&serial[port], '\r');
    SerialSend(&serial[port], ch);
}

void SeralPutString(int port, char *str)
{
    char ch;
    char *p = str;

    while (p)
    {
        ch = *p++;
        SerialPutChar(port, ch);
        if (ch == '\n')
            break;
    }
}

char SerialGetChar(int port)
{
    char ch;

    SerialReceived(&serial[port], &ch);
    if (ch == '\r')
        ch = '\n';
    return ch;
}

void SerialGetString(int port, char *str)
{
    char ch;
    char *p = str;

    while ((ch = SerialGetChar(port)) != '\n')
        *p++ = ch;
    *p = '\n';
}

void SerialSetBaud(int port, uint8_t baud)
{
    serial_hardware_t *obj = &serial[port];
    // baud divisor
    uint32_t baud_divisor[BAUD_MAX_NUM] = {2304, 1047, 524, 384, 192, 96, 48, 24, 12, 6, 3, 2, 1};
    uint32_t divisor = baud_divisor[baud];
    uint8_t temp = In8(obj->line_ctrl_reg);

    Out8(obj->line_ctrl_reg, (uint8_t)(LINE_DLAB | temp));
    Out8(obj->divisor_low_reg, (uint8_t)divisor);
    Out8(obj->divisor_high_reg, (uint8_t)(divisor >> 8));
    Out8(obj->line_ctrl_reg, temp);
}

void SerialSetMode(int port, uint8_t word, uint8_t stop, uint8_t parity)
{
    serial_hardware_t *obj = &serial[port];
    uint32_t word_len[WORD_MAX_NUM] = {LINE_WORD_LEN_5, LINE_WORD_LEN_6, LINE_WORD_LEN_7, LINE_WORD_LEN_8};
    uint32_t stop_bits[STOP_MAX_NUM] = {LINE_STOP_BIT_1, LINE_STOP_BIT_2};
    uint32_t parity_bits[PARITY_MAX_NUM] = {LINE_PARITY_NO, LINE_PARITY_ODD, LINE_PARITY_EVEN, LINE_PARITY_MARK, LINE_PARITY_SPACE};

    Out8(obj->line_ctrl_reg, word_len[word] | stop_bits[stop] | parity_bits[parity]);
}
